S06-05 IO流-项目:坦克大战
[TOC]
阶段一:绘制游戏地图和坦克
在动手敲代码之前,我们先来明确 Java 的绘图坐标体系。这和我们数学里的坐标轴有点不一样:
- 坐标原点 (0, 0) 位于屏幕的左上角,以像素为单位。
x轴向右延伸,y轴向下延伸。
Java 帮我们把画画的组件封装好了。我们需要用到 JFrame(游戏大窗口)和 JPanel(我们的画布),以及 Graphics(这相当于我们的画笔)。
现在,请在你的 IDE 中创建一个项目 TankGame,然后创建以下三个核心类:
坦克的基类 Tank
这是所有坦克的祖先,目前它只需要记录坦克的 (x, y) 坐标。
package com.yourname.tankgame;
/**
* 坦克的父类
*/
public class Tank {
private int x; // 坦克的横坐标
private int y; // 坦克的纵坐标
public Tank(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
}玩家的坦克 Hero
继承自 Tank,代表你亲自操控的英雄。
package com.yourname.tankgame;
/**
* 我方坦克
*/
public class Hero extends Tank {
public Hero(int x, int y) {
super(x, y);
}
}游戏核心画布与主窗口 TankGame
这里我们把画布(MyPanel)和窗口合并到一个文件里,方便你运行。我们在里面手绘一辆精美的像素坦克!
package com.yourname.tankgame;
import javax.swing.*;
import java.awt.*;
/**
* 坦克大战游戏主窗口
*/
public class TankGame extends JFrame {
private MyPanel mp = null;
public static void main(String[] args) {
new TankGame();
}
public TankGame() {
mp = new MyPanel();
this.add(mp); // 把画布塞进窗口
this.setSize(1000, 750); // 设置游戏窗口大小
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 点击关闭彻底退出程序
this.setVisible(true); // 让窗口可见
}
}
/**
* 游戏绘图区域(画布)
*/
class MyPanel extends JPanel {
Hero hero = null;
public MyPanel() {
hero = new Hero(100, 100); // 在坐标 (100, 100) 处初始化我方坦克
}
@Override
public void paint(Graphics g) {
super.paint(g);
// 1. 铺上黑色的战场背景
g.fillRect(0, 0, 1000, 750); //
// 2. 绘制我方坦克(目前默认方向向上:0)
drawTank(hero.getX(), hero.getY(), g, 0, 1); //
}
/**
* 封装的绘制坦克方法
* @param x 坦克左上角 x 坐标
* @param y 坦克左上角 y 坐标
* @param g 画笔
* @param direct 坦克方向(0-上,1-右,2-下,3-左)
* @param type 坦克类型(0-敌人,1-我方)
*/
public void drawTank(int x, int y, Graphics g, int direct, int type) {
// 根据类型设置颜色
switch (type) {
case 0: // 敌人
g.setColor(Color.cyan); //
break;
case 1: // 我方
g.setColor(Color.yellow); //
break;
}
// 根据方向绘制坦克形状(目前只处理向上 0)
switch (direct) {
case 0: // 向上
g.fill3DRect(x, y, 10, 60, false); // 左履带
g.fill3DRect(x + 30, y, 10, 60, false); // 右履带
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身主体
g.fillOval(x + 10, y + 20, 20, 20); // 圆形炮塔
g.drawLine(x + 20, y + 30, x + 20, y); // 直线炮筒
break;
default:
System.out.println("其他方向等我们下一关再来解锁!"); //
}
}
}阶段二:多坦克渲染与键盘控制
请将你的项目代码按照以下逻辑进行升级:
升级基类 Tank
我们在基类中增加了 方向 (direct) 和 速度 (speed) 属性,并封装了四个方向的移动方法。
package com.yourname.tankgame;
/**
* 坦克父类(增强移动和方向控制)
*/
public class Tank {
private int x; // 坦克的横坐标
private int y; // 坦克的纵坐标
private int direct = 0; // 坦克方向:0-上,1-右,2-下,3-左
private int speed = 4; // 坦克移动速度
public Tank(int x, int y) {
this.x = x;
this.y = y;
}
// 四个方向的坐标改变方法
public void moveUp() { y -= speed; }
public void moveRight() { x += speed; }
public void moveDown() { y += speed; }
public void moveLeft() { x -= speed; }
// Getter 和 Setter
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public int getDirect() { return direct; }
public void setDirect(int direct) { this.direct = direct; }
public int getSpeed() { return speed; }
public void setSpeed(int speed) { this.speed = speed; }
}新增敌人坦克类 EnemyTank
单独创建敌人坦克类,继承自 Tank,方便以后扩展敌人特有的 AI 逻辑。
package com.yourname.tankgame;
/**
* 敌人坦克类
*/
public class EnemyTank extends Tank {
public EnemyTank(int x, int y) {
super(x, y);
}
}升级主程序与画布 TankGame
这是本次升级的核心。画布类 MyPanel 将实现 KeyListener 接口,用以监听键盘按键。同时,我们使用线程安全的 Vector 容器来统一管理敌人坦克。
package com.yourname.tankgame;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;
/**
* 坦克大战 2.0 版
*/
public class TankGame extends JFrame {
private MyPanel mp = null;
public static void main(String[] args) {
new TankGame();
}
public TankGame() {
mp = new MyPanel();
this.add(mp);
this.setSize(1000, 750);
// 【重要】让窗口监听键盘事件
this.addKeyListener(mp);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}
/**
* 游戏绘图区域(实现 KeyListener 接口)
*/
class MyPanel extends JPanel implements KeyListener {
Hero hero = null;
// 使用 Vector 存储敌人坦克,确保多线程安全
Vector<EnemyTank> enemyTanks = new Vector<>();
int enemyTankSize = 3; // 敌人坦克数量
public MyPanel() {
// 初始化我方坦克
hero = new Hero(500, 500);
// 初始化敌人坦克
for (int i = 0; i < enemyTankSize; i++) {
// 让他们横向排开
EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
enemyTank.setDirect(2); // 默认方向向下
enemyTanks.add(enemyTank);
}
}
@Override
public void paint(Graphics g) {
super.paint(g);
// 铺上黑色背景
g.fillRect(0, 0, 1000, 750);
// 1. 绘制我方坦克(黄色)
drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);
// 2. 遍历 Vector 绘制所有敌人坦克(青色)
for (int i = 0; i < enemyTanks.size(); i++) {
EnemyTank enemyTank = enemyTanks.get(i);
drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);
}
}
/**
* 增强版绘制坦克方法(支持多方向)
*/
public void drawTank(int x, int y, Graphics g, int direct, int type) {
switch (type) {
case 0: g.setColor(Color.cyan); break; // 敌人坦克
case 1: g.setColor(Color.yellow); break; // 我方坦克
}
// 根据不同方向,绘制精细的坦克组件坐标
switch (direct) {
case 0: // 向上
g.fill3DRect(x, y, 10, 60, false); // 左履带
g.fill3DRect(x + 30, y, 10, 60, false); // 右履带
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
g.fillOval(x + 10, y + 20, 20, 20); // 炮塔
g.drawLine(x + 20, y + 30, x + 20, y); // 炮筒
break;
case 1: // 向右
g.fill3DRect(x, y, 60, 10, false); // 上履带
g.fill3DRect(x, y + 30, 60, 10, false); // 下履带
g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
g.fillOval(x + 20, y + 10, 20, 20); // 炮塔
g.drawLine(x + 30, y + 20, x + 60, y + 20); // 炮筒
break;
case 2: // 向下
g.fill3DRect(x, y, 10, 60, false); // 左履带
g.fill3DRect(x + 30, y, 10, 60, false); // 右履带
g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
g.fillOval(x + 10, y + 20, 20, 20); // 炮塔
g.drawLine(x + 20, y + 30, x + 20, y + 60); // 炮筒
break;
case 3: // 向左
g.fill3DRect(x, y, 60, 10, false); // 上履带
g.fill3DRect(x, y + 30, 60, 10, false); // 下履带
g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
g.fillOval(x + 20, y + 10, 20, 20); // 炮塔
g.drawLine(x + 30, y + 20, x, y + 20); // 炮筒
break;
default:
System.out.println("未知方向");
}
}
// 键盘按下事件监听
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_W) { // 向上移动
hero.setDirect(0);
hero.moveUp();
} else if (e.getKeyCode() == KeyEvent.VK_D) { // 向右移动
hero.setDirect(1);
hero.moveRight();
} else if (e.getKeyCode() == KeyEvent.VK_S) { // 向下移动
hero.setDirect(2);
hero.moveDown();
} else if (e.getKeyCode() == KeyEvent.VK_A) { // 向左移动
hero.setDirect(3);
hero.moveLeft();
}
// 【关键】每次改变坐标后,必须手动重绘面板,否则画面静止不变!
this.repaint();
}
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
}运行与测试说明
多坦克渲染:启动游戏后,除了中央的黄色我方坦克,你的屏幕上方还会整齐地排开 3 辆青色、炮口朝下的敌人坦克。
键盘控制:在游戏窗口处于激活状态时,按下键盘上的 W、A、S、D,你会发现你的黄色坦克会实时调整车身和炮口的方向,并在黑色的战场中流畅地四处移动。
阶段三:炮火连天,线程出击
这一步是整个坦克大战最精彩、也是技术含量的最高阶段之一!我们要将多线程(Multithreading)技术完美融入项目中。
在之前的版本里,只有你按下键盘,画面才会刷新。但现在,子弹要飞、敌人要动、碰撞要实时检测,这就需要战场拥有一个每时每刻都在自行运转的“主心骨”——游戏主线程。
根据项目设计要求,本阶段我们要攻克以下核心思路:
子弹(Shot)成为独立线程:按下
J键发射,子弹自主向前飞行,撞墙或击中目标后消亡。我方发射限制:控制弹药数量,最多同时发射 5 颗子弹。
敌人坦克自主乱动并开火:敌人坦克实现
Runnable接口,随机改变方向、随机开火(最多 3 颗子弹)。碰撞检测:主线程每时每刻都在计算“子弹是否击中坦克”,击中则令其消亡。
请在项目中补充或修改以下四个核心类:
新增子弹类 Shot
子弹必须实现 Runnable 接口,拥有自己的生命周期。
package com.yourname.tankgame;
/**
* 子弹类:独立线程,负责子弹的轨迹计算
*/
public class Shot implements Runnable {
private int x; // 子弹横坐标
private int y; // 子弹纵坐标
private int direct; // 子弹方向
private int speed = 6; // 子弹速度
private boolean isLive = true; // 子弹是否存活
public Shot(int x, int y, int direct) {
this.x = x;
this.y = y;
this.direct = direct;
}
@Override
public void run() {
while (isLive) {
// 休眠一下,防止子弹飞得太快变成“瞬移”
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 根据方向改变坐标
switch (direct) {
case 0: y -= speed; break; // 上
case 1: x += speed; break; // 右
case 2: y += speed; break; // 下
case 3: x -= speed; break; // 左
}
// 边界检测:子弹飞出地图则销毁
if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750)) {
isLive = false;
break;
}
}
}
// Getter & Setter
public int getX() { return x; }
public int getY() { return y; }
public boolean isLive() { return isLive; }
public void setLive(boolean live) { isLive = live; }
}升级我方坦克 Hero
允许我方坦克发射子弹,并用 Vector 存储,限制同屏最多 5 颗。
package com.yourname.tankgame;
import java.util.Vector;
public class Hero extends Tank {
// 存储我方发射的子弹
Vector<Shot> shots = new Vector<>();
public Hero(int x, int y) {
super(x, y);
}
/**
* 发射子弹的方法
*/
public void fireShot() {
// 控制最多发射 5 颗子弹
if (shots.size() >= 5) {
return;
}
Shot shot = null;
// 根据当前坦克的方向和位置,计算炮口坐标作为子弹的起点
switch (getDirect()) {
case 0: shot = new Shot(getX() + 20, getY(), 0); break;
case 1: shot = new Shot(getX() + 60, getY() + 20, 1); break;
case 2: shot = new Shot(getX() + 20, getY() + 60, 2); break;
case 3: shot = new Shot(getX(), getY() + 20, 3); break;
}
shots.add(shot);
// 【关键】启动子弹线程
new Thread(shot).start();
}
}升级敌人坦克 EnemyTank
敌人坦克不仅会随机移动,还会自动朝着当前炮口方向开火(最多 3 颗)。为了防止乱跑出界,增加了移动边界限制。
package com.yourname.tankgame;
import java.util.Vector;
/**
* 敌人坦克:实现 Runnable,能自主随机移动和开火
*/
public class EnemyTank extends Tank implements Runnable {
Vector<Shot> shots = new Vector<>();
private boolean isLive = true;
public EnemyTank(int x, int y) {
super(x, y);
}
@Override
public void run() {
while (isLive) {
// 如果子弹消亡了且同屏少于3颗,就补充子弹
if (isLive && shots.size() < 3) {
Shot s = null;
switch (getDirect()) {
case 0: s = new Shot(getX() + 20, getY(), 0); break;
case 1: s = new Shot(getX() + 60, getY() + 20, 1); break;
case 2: s = new Shot(getX() + 20, getY() + 60, 2); break;
case 3: s = new Shot(getX(), getY() + 20, 3); break;
}
shots.add(s);
new Thread(s).start();
}
// 根据当前方向,让坦克朝该方向走一段距离(比如30步)
for (int i = 0; i < 30; i++) {
try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
// 带有边界限制的移动
switch (getDirect()) {
case 0: if (getY() > 0) moveUp(); break;
case 1: if (getX() + 60 < 1000) moveRight(); break;
case 2: if (getY() + 60 < 750) moveDown(); break;
case 3: if (getX() > 0) moveLeft(); break;
}
}
// 随机改变方向(0-3)
setDirect((int) (Math.random() * 4));
}
}
public boolean isLive() { return isLive; }
public void setLive(boolean live) { isLive = live; }
}升级画布与重构主程序 TankGame
让画布 MyPanel 实现 Runnable 接口作为游戏的主循环(每 100ms 刷新一次战场),并在此处理所有的子弹清除与碰撞检测。
package com.yourname.tankgame;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;
/**
* 坦克大战 0.4 线程版
*/
public class TankGame extends JFrame {
private MyPanel mp = null;
public static void main(String[] args) {
new TankGame();
}
public TankGame() {
mp = new MyPanel();
this.add(mp);
this.setSize(1000, 750);
this.addKeyListener(mp);
// 【核心改变】启动画布(主游戏循环)线程
new Thread(mp).start();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}
/**
* 游戏画布:作为主线程,负责刷新和碰撞检测
*/
class MyPanel extends JPanel implements KeyListener, Runnable {
Hero hero = null;
Vector<EnemyTank> enemyTanks = new Vector<>();
int enemyTankSize = 3;
public MyPanel() {
hero = new Hero(500, 500);
for (int i = 0; i < enemyTankSize; i++) {
EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
enemyTank.setDirect(2);
enemyTanks.add(enemyTank);
// 【核心改变】启动敌人坦克线程,让他们自己动起来!
new Thread(enemyTank).start();
}
}
@Override
public void run() {
// 主线程循环:每 100ms 重绘并检测碰撞
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检测我方子弹是否击中敌人
hitEnemyTank();
// 重绘画面
this.repaint();
}
}
/**
* 检测我方所有子弹是否击中敌方坦克
*/
public void hitEnemyTank() {
for (int i = 0; i < hero.shots.size(); i++) {
Shot shot = hero.shots.get(i);
if (shot != null && shot.isLive()) { // 子弹还活着
for (int j = 0; j < enemyTanks.size(); j++) {
EnemyTank enemyTank = enemyTanks.get(j);
if (enemyTank.isLive()) {
// 调用碰撞算法
checkHit(shot, enemyTank);
}
}
}
}
}
/**
* 具体碰撞算法:判断子弹是否进入坦克的矩形范围
*/
public void checkHit(Shot s, EnemyTank tank) {
// 判定坦克的上下左右边界(注意方向不同,宽高互换)
int width = (tank.getDirect() == 0 || tank.getDirect() == 2) ? 40 : 60;
int height = (tank.getDirect() == 0 || tank.getDirect() == 2) ? 60 : 40;
if (s.getX() > tank.getX() && s.getX() < tank.getX() + width
&& s.getY() > tank.getY() && s.getY() < tank.getY() + height) {
s.setLive(false); // 子弹消亡
tank.setLive(false); // 坦克消亡
enemyTanks.remove(tank); // 从容器中移除敌人
}
}
@Override
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 1000, 750); // 黑色战场
// 1. 绘制我方坦克
drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);
// 2. 绘制我方发射的所有有效子弹
for (int i = 0; i < hero.shots.size(); i++) {
Shot shot = hero.shots.get(i);
if (shot != null && shot.isLive()) {
g.fillOval(shot.getX() - 2, shot.getY() - 2, 4, 4); // 画个小圆形当子弹
} else {
hero.shots.remove(shot); // 清理死掉的子弹
}
}
// 3. 绘制敌方坦克和他们的子弹
for (int i = 0; i < enemyTanks.size(); i++) {
EnemyTank enemyTank = enemyTanks.get(i);
if (enemyTank.isLive()) {
drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);
// 遍历绘制该敌人的子弹
for (int j = 0; j < enemyTank.shots.size(); j++) {
Shot shot = enemyTank.shots.get(j);
if (shot != null && shot.isLive()) {
g.setColor(Color.cyan);
g.fillOval(shot.getX() - 2, shot.getY() - 2, 4, 4);
} else {
enemyTank.shots.remove(shot); // 清理死子弹
}
}
}
}
}
// 绘制坦克的 drawTank 方法保持与第二步一致即可...
public void drawTank(int x, int y, Graphics g, int direct, int type) {
switch (type) {
case 0: g.setColor(Color.cyan); break;
case 1: g.setColor(Color.yellow); break;
}
switch (direct) {
case 0: // 上
g.fill3DRect(x, y, 10, 60, false);
g.fill3DRect(x + 30, y, 10, 60, false);
g.fill3DRect(x + 10, y + 10, 20, 40, false);
g.fillOval(x + 10, y + 20, 20, 20);
g.drawLine(x + 20, y + 30, x + 20, y);
break;
case 1: // 右
g.fill3DRect(x, y, 60, 10, false);
g.fill3DRect(x, y + 30, 60, 10, false);
g.fill3DRect(x + 10, y + 10, 40, 20, false);
g.fillOval(x + 20, y + 10, 20, 20);
g.drawLine(x + 30, y + 20, x + 60, y + 20);
break;
case 2: // 下
g.fill3DRect(x, y, 10, 60, false);
g.fill3DRect(x + 30, y, 10, 60, false);
g.fill3DRect(x + 10, y + 10, 20, 40, false);
g.fillOval(x + 10, y + 20, 20, 20);
g.drawLine(x + 20, y + 30, x + 20, y + 60);
break;
case 3: // 左
g.fill3DRect(x, y, 60, 10, false);
g.fill3DRect(x, y + 30, 60, 10, false);
g.fill3DRect(x + 10, y + 10, 40, 20, false);
g.fillOval(x + 20, y + 10, 20, 20);
g.drawLine(x + 30, y + 20, x, y + 20);
break;
}
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_W) { hero.setDirect(0); hero.moveUp(); }
else if (e.getKeyCode() == KeyEvent.VK_D) { hero.setDirect(1); hero.moveRight(); }
else if (e.getKeyCode() == KeyEvent.VK_S) { hero.setDirect(2); hero.moveDown(); }
else if (e.getKeyCode() == KeyEvent.VK_A) { hero.setDirect(3); hero.moveLeft(); }
// 【新增】监听 J 键发射子弹
if (e.getKeyCode() == KeyEvent.VK_J) {
hero.fireShot();
}
}
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
}这一阶段的运行效果
游戏一启动,敌人坦克就会开始四处乱晃,并且会自动朝你发射青色的小子弹。
你可以操控黄色坦克躲避,同时按下 J 键对准敌人开火。当你发出的子弹撞上敌人坦克时,敌人坦克会立刻从地图上消失!
如果连续狂按 J 键,屏幕上最多只会有 5 颗你打出的子弹,达到了控弹效果。
阶段四:存档与音乐
在这一步中,我们将解决两个核心痛点:一是让游戏具有“记忆力”(使用 I/O 流实现持久化存储,记录击毁数和坦克坐标);二是让游戏具有“沉浸感”(使用多线程播放背景音乐)。
核心功能设计思路
数据持久化(I/O 流):
- 创建一个
Recorder记录类。当玩家退出游戏时,使用BufferedWriter将玩家的击毁敌方坦克数、地图上剩余敌人坦克的(x, y, direct)写入到文本文件myRecord.txt中。 - 游戏启动时,通过弹窗让玩家选择“新游戏”或“继续上局游戏”。如果选择继续,则使用
BufferedReader读取文件,恢复上局的进度。
- 创建一个
多线程音频播放:
- 创建一个专门用于播放音频的线程类(如
AePlayWave),利用 Java 的AudioSystem读取.wav格式的音频文件,实现游戏开始时自动循环播放 BGM。
- 创建一个专门用于播放音频的线程类(如
新增记录器类 Recorder
这个类负责处理所有与文件 I/O 相关的存盘与读盘逻辑。
package com.yourname.tankgame;
import java.io.*;
import java.util.Vector;
/**
* 记录器类:负责持久化游戏数据与状态
*/
public class Recorder {
// 记录我方击毁敌方坦克总数
private static int allEnemyTankNum = 0;
// I/O 流定义
private static BufferedWriter bw = null;
private static BufferedReader br = null;
private static String recordFile = "src/myRecord.txt"; // 存档文件路径
// 指向画布中的敌人坦克 Vector,用于获取存盘时的坐标
private static Vector<EnemyTank> enemyTanks = null;
// 用于恢复数据的容器
private static Vector<Node> nodes = new Vector<>();
public static void setEnemyTanks(Vector<EnemyTank> enemyTanks) {
Recorder.enemyTanks = enemyTanks;
}
public static int getAllEnemyTankNum() { return allEnemyTankNum; }
public static void setAllEnemyTankNum(int allEnemyTankNum) { Recorder.allEnemyTankNum = allEnemyTankNum; }
// 击毁敌人坦克时,计数加 1
public static void addAllEnemyTankNum() { Recorder.allEnemyTankNum++; }
/**
* 恢复数据:读取 myRecord.txt 文件
*/
public static Vector<Node> getNodesAndRecord() {
try {
br = new BufferedReader(new FileReader(recordFile));
// 读取第一行:击毁总数
allEnemyTankNum = Integer.parseInt(br.readLine());
// 循环读取后续行:敌人坦克的坐标和方向
String line = "";
while ((line = br.readLine()) != null) {
String[] xyd = line.split(" ");
Node node = new Node(
Integer.parseInt(xyd[0]),
Integer.parseInt(xyd[1]),
Integer.parseInt(xyd[2])
);
nodes.add(node);
}
} catch (IOException e) {
System.out.println("未找到历史存档,请开启新游戏!");
} finally {
try {
if (br != null) br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return nodes;
}
/**
* 存盘退出:将游戏数据写入文件
*/
public static void keepRecord() {
try {
bw = new BufferedWriter(new FileWriter(recordFile));
// 1. 写入击毁数
bw.write(allEnemyTankNum + "\r\n");
// 2. 遍历当前存活的敌人坦克,保存坐标和方向
if (enemyTanks != null) {
for (int i = 0; i < enemyTanks.size(); i++) {
EnemyTank enemyTank = enemyTanks.get(i);
if (enemyTank.isLive()) {
String record = enemyTank.getX() + " " + enemyTank.getY() + " " + enemyTank.getDirect();
bw.write(record + "\r\n");
}
}
}
System.out.println("游戏数据保存成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 辅助内部类:专门用于临时存放读取出来的坦克信息
*/
class Node {
private int x;
private int y;
private int direct;
public Node(int x, int y, int direct) {
this.x = x;
this.y = y;
this.direct = direct;
}
public int getX() { return x; }
public int getY() { return y; }
public int getDirect() { return direct; }
}新增音频播放类 AePlayWave
这是一个标准的声音流播放器线程,支持传入外部的 .wav 文件并独立于主游戏运行。
package com.yourname.tankgame;
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
/**
* 音乐播放线程
*/
public class AePlayWave extends Thread {
private String filename;
public AePlayWave(String wavfile) {
filename = wavfile;
}
@Override
public void run() {
File soundFile = new File(filename);
if (!soundFile.exists()) {
System.err.println("Wave file not found: " + filename);
return;
}
AudioInputStream audioInputStream = null;
try {
audioInputStream = AudioSystem.getAudioInputStream(soundFile);
} catch (UnsupportedAudioFileException | IOException e) {
e.printStackTrace();
return;
}
AudioFormat format = audioInputStream.getFormat();
SourceDataLine auline = null;
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
try {
auline = (SourceDataLine) AudioSystem.getLine(info);
auline.open(format);
} catch (LineUnavailableException e) {
e.printStackTrace();
return;
}
auline.start();
int nBytesRead = 0;
byte[] abData = new byte[512];
try {
while (nBytesRead != -1) {
nBytesRead = audioInputStream.read(abData, 0, abData.length);
if (nBytesRead >= 0) {
auline.write(abData, 0, nBytesRead);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
auline.drain();
auline.close();
}
}
}升级主入口 TankGame
在主窗口启动时,我们加入一个选项对话框让玩家选择模式。同时,通过监听窗口关闭事件,在玩家关闭窗口时触发自动存盘。
package com.yourname.tankgame;
import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Scanner;
/**
* 最终整合版:加入存盘与音乐控制
*/
public class TankGame extends JFrame {
private MyPanel mp = null;
public static void main(String[] args) {
new TankGame();
}
public TankGame() {
// 1. 游戏启动时提示选择
String key = JOptionPane.showInputDialog("请选择: 1. 新游戏 2. 继续上局游戏");
if (key == null) key = "1"; // 默认开启新游戏
mp = new MyPanel(key);
this.add(mp);
this.setSize(1000, 750);
this.addKeyListener(mp);
new Thread(mp).start(); // 启动主刷新线程
// 2. 播放游戏背景音乐
// 请确保在项目根目录或指定路径下放一个真实的 111.wav 音乐文件
new AePlayWave("src/111.wav").start();
// 3. 【核心】监听窗口关闭,关闭时进行自动存盘
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
Recorder.keepRecord(); // 执行存盘
System.exit(0);
}
});
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}升级游戏画布类 MyPanel
修改 MyPanel 的构造函数,让它根据玩家的选择来决定是生成全新的敌人,还是从 Recorder 的 nodes 列表中读取上局的坐标进行还原。
// 在 MyPanel 类中修改构造器:
public MyPanel(String key) {
hero = new Hero(500, 500);
// 将当前面板的敌人容器交给 Recorder 集中管理
Recorder.setEnemyTanks(enemyTanks);
if (key.equals("2")) { // 继续上局游戏
Vector<Node> nodes = Recorder.getNodesAndRecord();
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
EnemyTank enemyTank = new EnemyTank(node.getX(), node.getY());
enemyTank.setDirect(node.getDirect());
enemyTanks.add(enemyTank);
new Thread(enemyTank).start();
}
} else { // 开启全新游戏
for (int i = 0; i < enemyTankSize; i++) {
EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 100);
enemyTank.setDirect(2);
enemyTanks.add(enemyTank);
new Thread(enemyTank).start();
}
}
}
// 别忘了:在你的 checkHit 击毁敌方坦克的逻辑中,成功击中后要加上一句话:
// Recorder.addAllEnemyTankNum(); // 战绩加 1最终版本测试指南
音乐起航:准备一个
.wav格式的音效文件,重命名为111.wav放在src/目录下。启动游戏,激昂的战歌就会随之响起!战绩与存盘:进入游戏后干掉几辆敌方坦克,然后直接点击窗口的
X按钮关闭游戏。此时去查看你的src/目录,你会发现自动生成了一个myRecord.txt文件,里面精确保存了你的击毁总数和战场残余敌人的坐标。完美复活:再次启动游戏,在弹窗中输入
2。瞧!上一局没打完的敌人坦克会在它们原本断气/停驻的位置重新复活,战绩也得到了继承。
至此,一个功能完整、技术全面的 Java 坦克大战迭代项目 就彻底完工了!你从零搭建了坐标系、引入了键盘监听、攻克了多线程并发子弹、最终用 I/O 流锁住了数据。这绝对是你 Java 进阶之路上里程碑式的一步。